import glmatrix = require("gl-matrix");
import {TileCoord} from "../source/TileCoord";
import {LngLat} from "./LngLat";
import {Util} from "../util/Util";
const vec4 = glmatrix.vec4,
  vec2 = glmatrix.vec2,
  mat4 = glmatrix.mat4,
  mat2 = glmatrix.mat2;

export class Transform {
  tileSize: number; // VT的像素大小
  tileZoom: number;
  lngRange?: [number, number];
  latRange?: [number, number];
  scale: number;
  width: number;  // canvas size
  height: number;
  angle: number;  // 旋转角度，单位是弧度，绕Z轴旋转
  rotationMatrix: glmatrix.mat2;
  zoomFraction: number;
  pixelsToGLUnits: Array<number>;
  cameraToCenterDistance: number;
  projMatrix: glmatrix.mat4;
  pixelMatrix: glmatrix.mat4;
  pixelMatrixInverse: glmatrix.mat4;
  _fov: number;  // field of view
  _pitch: number; // 倾斜角度，单位是弧度，绕X轴旋转
  _zoom: number;
  _unmodified: boolean;
  _renderWorldCopies: boolean;
  _minZoom: number;
  _maxZoom: number;
  _center: LngLat;
  _constraining: boolean;

  constructor(minZoom?: number, maxZoom?: number, renderWorldCopies?: boolean | void) {
    this.tileSize = 512; // constant

    this._renderWorldCopies = renderWorldCopies === undefined ? true : renderWorldCopies;
    this._minZoom = minZoom || 0;
    this._maxZoom = maxZoom || 22;

    this.latRange = [-85.05113, 85.05113]; // 纬度范围

    this.width = 0;
    this.height = 0;
    this._center = new LngLat(0, 0);
    this.zoom = 0;
    this.angle = 0;
    this._fov = 0.6435011087932844;
    this._pitch = 0;
    this._unmodified = true;
  }

  get minZoom(): number { return this._minZoom; }
  set minZoom(zoom: number) {
    if (this._minZoom === zoom) return;
    this._minZoom = zoom;
    this.zoom = Math.max(this.zoom, zoom);
  }

  get maxZoom(): number { return this._maxZoom; }
  set maxZoom(zoom: number) {
    if (this._maxZoom === zoom) return;
    this._maxZoom = zoom;
    this.zoom = Math.min(this.zoom, zoom);
  }

  get renderWorldCopies(): boolean {
    return this._renderWorldCopies;
  }

  get worldSize(): number {
    return this.tileSize * this.scale;
  }

  /**
   * 像素值的中心坐标
   *
   * @readonly
   * @memberof Transform
   */
  get centerPoint() {
    const out = vec2.create();
    vec2.div(out, this.size, vec2.fromValues(2, 2));
    return out;
  }

  get size() {
    return vec2.fromValues(this.width, this.height);
  }

  get bearing(): number {
    return -this.angle / Math.PI * 180;
  }
  set bearing(bearing: number) {
    const b = -Util.wrap(bearing, -180, 180) * Math.PI / 180;
    if (this.angle === b) {
      return;
    }
    this._unmodified = false;
    this.angle = b;
    this._calcMatrices();

    // 2x2 matrix for rotating points
    this.rotationMatrix = mat2.create();
    mat2.rotate(this.rotationMatrix, this.rotationMatrix, this.angle);
  }

  get pitch(): number {
    return this._pitch / Math.PI * 180;
  }
  set pitch(pitch: number) {
    const p = Util.clamp(pitch, 0, 60) / 180 * Math.PI;
    if (this._pitch === p) return;
    this._unmodified = false;
    this._pitch = p;
    this._calcMatrices();
  }

  get fov(): number {
    return this._fov / Math.PI * 180;
  }
  set fov(fov: number) {
    fov = Util.clamp(fov, 0.01, 60); // Math.max(0.01, Math.min(60, fov));
    if (this._fov === fov) return;
    this._unmodified = false;
    this._fov = fov / 180 * Math.PI;
    this._calcMatrices();
  }

  get zoom(): number { return this._zoom; }
  set zoom(zoom: number) {
    const z = Util.clamp(zoom, this.minZoom, this.maxZoom);
    // Math.min(Math.max(zoom, this.minZoom), this.maxZoom);
    if (this._zoom === z) return;
    this._unmodified = false;
    this._zoom = z;
    this.scale = this.zoomScale(z);
    this.tileZoom = Math.floor(z); // z可能是一个浮点数
    this.zoomFraction = z - this.tileZoom;
    this._constrain();
    this._calcMatrices();
  }

  get center(): LngLat { return this._center; }
  set center(center: LngLat) {
    if (center.lat === this._center.lat && center.lng === this._center.lng) {
      return;
    }
    this._unmodified = false;
    this._center = center;
    this._constrain();
    this._calcMatrices();
  }

  get x(): number { return this.lngX(this.center.lng); }
  get y(): number { return this.latY(this.center.lat); }

  resize(width: number, height: number) {
    this.width = width;
    this.height = height;

    this.pixelsToGLUnits = [2 / width, -2 / height];
    this._constrain();
    this._calcMatrices();
  }

  get unmodified(): boolean { return this._unmodified; }

  zoomScale(zoom: number) { return Math.pow(2, zoom); }
  scaleZoom(scale: number) { return Math.log(scale) / Math.LN2; }

  /**
   * Return a zoom level that will cover all tiles the transform
   * @param {Object} options
   * @param {number} options.tileSize
   * @param {boolean} options.roundZoom
   * @returns {number} zoom level
   */
  coveringZoomLevel(options: {roundZoom?: boolean, tileSize: number}) {
    return (options.roundZoom ? Math.round : Math.floor)(
      this.zoom + this.scaleZoom(this.tileSize / options.tileSize)
    );
  }

  project(lnglat: LngLat): glmatrix.vec2 {
    return vec2.fromValues(
        this.lngX(lnglat.lng),
        this.latY(lnglat.lat));
  }

  unproject(point: glmatrix.vec2) {
      return new LngLat(
          this.xLng(point[0]),
          this.yLat(point[1]));
  }
  /**
   * longitude to absolute x coord
   * @param {number} lon
   * @returns {number} pixel coordinate
   */
  lngX(lng: number) {
    return (180 + lng) * this.worldSize / 360;
  }
  /**
   * latitude to absolute y coord，通过墨卡托投影的转换公式
   * @param {number} lat
   * @returns {number} pixel coordinate
   */
  latY(lat: number) {
      const y = 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360));
      return (180 - y) * this.worldSize / 360;
  }

  xLng(x: number) {
      return x * 360 / this.worldSize - 180;
  }
  yLat(y: number) {
      const y2 = 180 - y * 360 / this.worldSize;
      return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
  }

  /**
   * Calculate the posMatrix that, given a tile coordinate, would be used to display the tile on a map.
   * @param {TileCoord} tileCoord
   * @param {number} maxZoom maximum source zoom to account for overscaling
   */
  calculatePosMatrix(tileCoord: TileCoord, maxZoom?: number) {
    const EXTENT = 8192;
    // if z > maxzoom then the tile is actually a overscaled maxzoom tile,
    // so calculate the matrix the maxzoom tile would use.
    const coord = tileCoord.toCoordinate(maxZoom);
    const scale = this.worldSize / this.zoomScale(coord.zoom);

    const posMatrix = mat4.create();
    mat4.identity(posMatrix);
    mat4.translate(posMatrix, posMatrix, [coord.column * scale, coord.row * scale, 0]);
    mat4.scale(posMatrix, posMatrix, [scale / EXTENT, scale / EXTENT, 1]);
    mat4.multiply(posMatrix, this.projMatrix, posMatrix);

    return posMatrix;
  }

  /**
   * 修正center经纬度
   *
   * @returns
   * @memberof Transform
   */
  _constrain() {
    if (!this.center || !this.width || !this.height || this._constraining) return;

    this._constraining = true;

    let minY = -90;
    let maxY = 90;
    let minX = -180;
    let maxX = 180;
    let sy, sx, x2, y2;
    const size = this.size,
        unmodified = this._unmodified;

    if (this.latRange) {
        const latRange = this.latRange;
        minY = this.latY(latRange[1]);
        maxY = this.latY(latRange[0]);
        sy = maxY - minY < size[1] ? size[1] / (maxY - minY) : 0;
    }

    if (this.lngRange) {
        const lngRange = this.lngRange;
        minX = this.lngX(lngRange[0]);
        maxX = this.lngX(lngRange[1]);
        sx = maxX - minX < size[0] ? size[0] / (maxX - minX) : 0;
    }

    // how much the map should scale to fit the screen into given latitude/longitude ranges
    const s = Math.max(sx || 0, sy || 0);

    if (s) {
      this.center = this.unproject(vec2.fromValues(
          sx ? (maxX + minX) / 2 : this.x,
          sy ? (maxY + minY) / 2 : this.y));
      this.zoom += this.scaleZoom(s);
      this._unmodified = unmodified;
      this._constraining = false;
      return;
    }

    if (this.latRange) {
        const y = this.y,
            h2 = size[1] / 2;

        if (y - h2 < minY) y2 = minY + h2;
        if (y + h2 > maxY) y2 = maxY - h2;
    }

    if (this.lngRange) {
        const x = this.x,
            w2 = size[0] / 2;

        if (x - w2 < minX) x2 = minX + w2;
        if (x + w2 > maxX) x2 = maxX - w2;
    }

    // pan the map if the screen goes off the range
    if (x2 !== undefined || y2 !== undefined) {
        this.center = this.unproject(vec2.fromValues(
            x2 !== undefined ? x2 : this.x,
            y2 !== undefined ? y2 : this.y));
    }

    this._unmodified = unmodified;
    this._constraining = false;
  }

  _calcMatrices() {
    if (!this.height) return;

    this.cameraToCenterDistance = 0.5 / Math.tan(this._fov / 2) * this.height;

    // Find the distance from the center point [width/2, height/2] to the
    // center top point [width/2, 0] in Z units, using the law of sines.
    // 1 Z unit is equivalent to 1 horizontal px at the center of the map
    // (the distance between[width/2, height/2] and [width/2 + 1, height/2])
    const halfFov = this._fov / 2;
    const groundAngle = Math.PI / 2 + this._pitch;
    const topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(Math.PI - groundAngle - halfFov);

    // Calculate z distance of the farthest fragment that should be rendered.
    const furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance;
    // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
    const farZ = furthestDistance * 1.01;

    // matrix for conversion from location to GL coordinates (-1 .. 1)
    let m = mat4.create();
    mat4.perspective(m, this._fov, this.width / this.height, 1, farZ);

    mat4.scale(m, m, [1, -1, 1]); // y轴翻转
    mat4.translate(m, m, [0, 0, -this.cameraToCenterDistance]);
    mat4.rotateX(m, m, this._pitch);
    mat4.rotateZ(m, m, this.angle);
    mat4.translate(m, m, [-this.x, -this.y, 0]);

    // scale vertically to meters per pixel (inverse of ground resolution):
    // worldSize / (circumferenceOfEarth * cos(lat * π / 180))
    const verticalScale = this.worldSize / (2 * Math.PI * 6378137 * Math.abs(Math.cos(this.center.lat * (Math.PI / 180))));
    mat4.scale(m, m, [1, 1, verticalScale, 1]);

    this.projMatrix = m;

    // matrix for conversion from location to screen coordinates
    m = mat4.create();
    mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
    mat4.translate(m, m, [1, -1, 0]);
    this.pixelMatrix = mat4.multiply(mat4.create(), m, this.projMatrix);

    // inverse matrix for conversion from screen coordinaes to location
    m = mat4.create();
    mat4.invert(m, this.pixelMatrix);
    if (!m) throw new Error("failed to invert matrix");
    this.pixelMatrixInverse = m;
  }
}